Extend the SuiteQL Query Tool with custom functionality using the plugin architecture
The SuiteQL Query Tool supports a plugin architecture that allows developers to extend functionality without modifying the main script. Plugins can:
Plugins are JavaScript files that export a plugin definition object. Files must be named with the .sqt-plugin.js extension (or .sqt-plugin.json for JSON-only plugins).
CONFIG.PLUGIN_FOLDER_ID in the suitelet to this folder's ID(function() {
return {
// Required fields
name: 'my-plugin',
version: '1.0.0',
// Optional fields
minAppVersion: '2026.1',
description: 'My custom plugin',
author: 'Your Name',
dependencies: [],
disables: [],
// Plugin functionality
server: { ... },
client: { ... },
ui: { ... },
settings: { ... }
};
})()
| Field | Type | Description |
|---|---|---|
name |
string | Unique identifier for the plugin |
version |
string | Plugin version (e.g., '1.0.0') |
| Field | Type | Description |
|---|---|---|
minAppVersion |
string | Minimum SQT version required |
description |
string | Plugin description |
author |
string | Plugin author |
dependencies |
array | Names of plugins that must load first |
disables |
array | Built-in features to hide |
The following feature names can be used in the disables array:
ai - All AI featuresai-chat - AI Chat modalai-explain - Explain Queryai-validate - Validate Queryai-nlbar - NL barexport - Export functionalityexport-airtable - Airtable exportexport-google-sheets - Google Sheetslocal-library - Local libraryremote-library - Remote libraryworkbooks - Workbookstables-reference - Tables Referenceschema-explorer - Schema Explorerdoc-generator - Doc Generatorshare - Share Queryhistory - Query Historydark-mode - Theme togglefocus-mode - Focus modeformat - Format Queryoptions - Options panelServer hooks are called during query execution on the NetSuite server.
server: {
hooks: {
// Called before query execution
// Can modify the query or cancel execution
onBeforeQuery: function(data, plugin) {
// data.query - the SQL to execute
// data.payload - the request payload
// data.originalQuery - unmodified query
return data; // Return modified data or undefined
},
// Called after successful query
onAfterQuery: function(data, plugin) {
// data.query - executed SQL
// data.payload - request payload
// data.response - query results
return data;
},
// Called when query execution fails
onError: function(data, plugin) {
// data.query - the SQL that failed
// data.payload - request payload
// data.error - the error object
}
}
}
Add custom server-side endpoints:
server: {
handlers: {
myHandler: function(context, payload, modules, plugin) {
// context - NetSuite context object
// payload - request payload
// modules - NetSuite modules (file, https, log, etc.)
// plugin - this plugin's definition
context.response.write(JSON.stringify({
result: 'success'
}));
}
}
}
fetch(scriptUrl, { body: JSON.stringify({ function: 'plugin_pluginname_myHandler', ... }) })
Client hooks are called in the browser.
client: {
hooks: {
// Called when app initialization completes
onInit: function(data) {
// data.state - app state object
// data.config - app config
return data;
},
// Called before query execution
onBeforeQuery: function(data) {
// data.query - SQL to execute
// data.options - query options
// Return { ...data, cancel: true } to cancel
return data;
},
// Called after successful query
onAfterQuery: function(data) {
// data.query - executed SQL
// data.results - query results
// data.elapsedTime - execution time
// data.rowCount - number of rows
return data;
},
// Called after results render to DOM
onResultsDisplay: function(data) {
// data.data - results data
// data.viewMode - current view mode
// data.panel - results panel element
return data;
},
// Called before export
onBeforeExport: function(data) {
// data.format - export format
// data.records - data to export
// Return { ...data, cancel: true } to cancel
return data;
},
// Called after export
onAfterExport: function(data) {
// data.format - export format
// data.rowCount - rows exported
return data;
},
// Called when editor content changes
onEditorChange: function(data) {
// data.content - current editor content
return data;
}
}
}
The init function runs when the plugin loads and can return a public API:
client: {
init: function(meta, pluginsApi) {
// meta - { name, version, description }
// pluginsApi - plugins object with saveSettings, loadSettings, etc.
// Return public API (optional)
return {
doSomething: function() { ... },
getStatus: function() { ... }
};
}
}
SQT.plugins.get('my-plugin').doSomething()
Inject HTML at designated points in the interface:
ui: {
'toolbar-start': '<button onclick="...">Custom</button>',
'toolbar-end': '<button onclick="...">Custom</button>',
'more-dropdown': '<div class="sqt-toolbar-dropdown-item">...</div>',
'ai-dropdown': '<div class="sqt-toolbar-dropdown-item">...</div>',
'header-right': '<button class="sqt-btn">...</button>',
'before-editor': '<div class="alert">Notice</div>',
'editor-toolbar': '<button class="btn btn-sm">...</button>',
'nl-bar': '<div>NL bar extension</div>',
'results-header': '<button>Custom action</button>',
'results-footer': '<div>Summary info</div>',
'export-menu': '<button onclick="...">Custom Export</button>',
'options-panel': '<div class="sqt-options-section">...</div>',
'sidebar-section': '<div>Custom sidebar content</div>',
'local-library-actions': '<button>...</button>',
'status-bar': '<span>Status indicator</span>',
'modals': '<div class="modal">...</div>'
}
| Point | Location |
|---|---|
toolbar-start | After Run button |
toolbar-end | Before options group |
more-dropdown | End of More dropdown menu |
ai-dropdown | End of AI dropdown menu |
| Point | Location |
|---|---|
header-right | Header actions area (before sidebar toggle) |
| Point | Location |
|---|---|
before-editor | Above the editor panel |
editor-toolbar | End of editor mini-toolbar |
nl-bar | After natural language bar |
| Point | Location |
|---|---|
results-header | Results header actions area |
results-footer | Below results content |
| Point | Location |
|---|---|
sidebar-section | End of sidebar |
export-menu | End of export modal |
local-library-actions | Local library modal header |
modals | After all built-in modals |
| Point | Location |
|---|---|
options-panel | End of options dropdown |
status-bar | Status bar left section |
Some injection points (results-header, results-footer) are rendered dynamically when results are displayed. For these, inject via the onResultsDisplay hook:
client: {
hooks: {
onResultsDisplay: function(data) {
// Inject into results header
var header = document.getElementById('sqtPluginResultsHeader');
if (header) {
header.innerHTML = '<button onclick="myAction()">Custom</button>';
}
// Inject into results footer
var footer = document.getElementById('sqtPluginResultsFooter');
if (footer) {
footer.innerHTML = '<div>Row count: ' + data.data.rowCount + '</div>';
}
return data;
}
}
}
// Save settings (localStorage only)
SQT.plugins.saveSettings('my-plugin', { enabled: true });
// Load settings
var settings = SQT.plugins.loadSettings('my-plugin');
// Save to server (persists across browsers)
SQT.plugins.saveSettings('my-plugin', settings, true);
Define a schema for automatic settings UI generation:
settings: {
schema: {
enabled: {
type: 'boolean',
default: true,
label: 'Enable Feature',
description: 'Toggle this feature on/off'
},
threshold: {
type: 'number',
default: 100,
label: 'Threshold',
description: 'Set the threshold value'
}
}
}
acme-audit-logger)minAppVersion and dependencies to ensure compatibilitySee query-logger.sqt-plugin.js in the sample-plugins folder for a complete example demonstrating:
.sqt-plugin.js)CONFIG.PLUGIN_FOLDER_ID is set correctlyonResultsDisplay hook[SQT Plugins]. Check the console for detailed error messages.